/**
 * @开发版权 
 * @项目名称 轻量数据中台(LDMP)
 * @版本信息 v1.0
 * @开发人员 zhous
 * @开发日期 2025-01-17
 * @修订日期
 * @描述  DiskController
 */
package com.ng.biz.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.Page;
import com.ng.biz.model.domain.Disk;
import com.ng.biz.model.minio.MinioFileInfo;
import com.ng.biz.service.IDisk;
import com.ng.common.annotation.TokenRequired;
import com.ng.common.annotation.Tx;
import com.ng.common.controller.Controller;
import com.ng.common.model.response.R;
import com.ng.common.util.DateUtil;
import com.ng.common.util.UuidUtil;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.ListObjectsArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import io.minio.messages.Item;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.io.FileUtils;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import java.util.concurrent.TimeUnit;

@Tag(name = "网盘管理")
@RequiredArgsConstructor
@RestController
@RequestMapping("/biz/disk")
@Slf4j
public class DiskController extends Controller {

	private final MinioClient minioClient;
	
	@Autowired
	private IDisk diskImpl;
	
	@Value(value = "${app.ffmpeg.path}")
	private String ffmpegPath;
	
	@Value(value = "${app.ffprobe.path}")
	private String ffprobePath;
	
	@Value(value = "${app.thumbnail.cache-dir}")
	private String thumbnailCacheDir;
	
	@Value("${minio.bucket-name}")
	private String bucketName;

	@Operation(summary = "获取分页获取文件/文件夹列表")
	@Parameters({ @Parameter(name = "keywords", description = "文件/文件夹名", required = false),
			@Parameter(name = "parentId", description = "上级主键", required = false),
			@Parameter(name = "type", description = "文件类型(字典标识:disk_file_type)", required = false),
			@Parameter(name = "cur", description = "当前页码", required = true),
			@Parameter(name = "limit", description = "每页数据量", required = true) })
	@TokenRequired
	@RequestMapping(value = "/pages", method = RequestMethod.GET)
	public R<Page<Disk>> pages(HttpServletRequest request,
			@RequestParam(name = "keywords", required = false, defaultValue = "") String keywords,
			@RequestParam(name = "parentId", required = false, defaultValue = "") String parentId,
			@RequestParam(name = "type", required = false, defaultValue = "") String type,
			@RequestParam(name = "cur", required = false, defaultValue = "1") int cur,
			@RequestParam(name = "limit", required = false, defaultValue = "20") int limit) {
		
		String sqlExceptSelect = "FROM " + Disk.TABLE_NAME + " WHERE 1=1";
		if (StrKit.notBlank(keywords)) {
			sqlExceptSelect += " AND name LIKE '%" + keywords + "%'";
		}
		if (StrKit.notBlank(parentId)) {
			sqlExceptSelect += " AND parent_id = '" + parentId + "'";
		}
		if (StrKit.notBlank(type)) {
			sqlExceptSelect += " AND type = '" + type + "'";
		}
		sqlExceptSelect += " ORDER BY sort ASC, create_time DESC";

		return R.ok(Disk.dao.paginate(cur, limit, "SELECT *", sqlExceptSelect));
	}
	
	@Operation(summary = "保存文件夹")
	@Parameters({ @Parameter(name = "disk", description = "diskFolder 文件夹", required = true) })
	@TokenRequired
	@Tx
	@RequestMapping(value = "/save", method = RequestMethod.POST)
	public R<Disk> saveDbapiGroup(HttpServletRequest request, @RequestBody Disk disk) {
		try {
			if (StrKit.notBlank(disk.getId())) {
				if(StrKit.isBlank(disk.getParentId())) {
					disk.setParentId("");
				}
				disk.setUpdateUserId(getUserId(request));
				disk.setUpdateTime(new Date(System.currentTimeMillis()));
				if (disk.update()) {
					return R.ok(disk);
				} else {
					return R.failed("更新失败！");
				}
			} else {
				disk.setId(UuidUtil.getUUID());
				if(StrKit.isBlank(disk.getParentId())) {
					disk.setParentId("");
				}
				disk.setCreateUserId(getUserId(request));
				disk.setCreateTime(new Date(System.currentTimeMillis()));
				if (disk.save()) {
					return R.ok(disk);
				} else {
					return R.failed("保存失败.");
				}
			}
		} catch (Exception e) {
			return R.failed(e.getMessage());
		}
	}

	@Operation(summary = "删除文件/文件夹")
	@Parameters({ @Parameter(name = "ids", description = "文件/文件夹Id集合", required = false) })
	@DeleteMapping("/{ids}")
	@TokenRequired
	public R<String> deleteByIds(@PathVariable("ids") String ids) {
		return diskImpl.deleteByIds(ids);
	}

	@PostMapping("/upload")
	@Operation(summary = "上传文件")
	@TokenRequired
	public R<Object> upload(@RequestParam(name = "file", required = false)  MultipartFile file, @RequestParam(name = "parentId", required = false) String parentId, HttpServletRequest request) {
		try {
			// 检查文件是否为空
			if (file == null || file.isEmpty()) {
				return R.failed("上传文件不能为空");
			}

			// 获取文件信息
			String originalFilename = file.getOriginalFilename();
			long fileSize = file.getSize();

			// 生成文件存储路径
			String objectName = generateObjectName(parentId, originalFilename);

			// 上传到MinIO
			minioClient.putObject(
				PutObjectArgs.builder()
					.bucket(bucketName)
					.object(objectName)
					.stream(file.getInputStream(), fileSize, -1)
					.contentType(file.getContentType())
					.build()
			);

			// 保存文件记录到数据库
			Disk disk = new Disk();
			disk.setId(UuidUtil.getUUID32());
			disk.setName(originalFilename);
			disk.setSize(fileSize);
			disk.setType(getFileType(originalFilename));
			disk.setPath(objectName);
			disk.setParentId(parentId);
			if(StrKit.isBlank(parentId)) {
				disk.setParentId("");
			}
			disk.setUpdateTime(new Date());
			disk.save();

			return R.ok();
		} catch (Exception e) {
			log.error("上传文件失败", e);
			return R.failed("上传文件失败: " + e.getMessage());
		}
	}

	// 生成文件存储路径
	private String generateObjectName(String parentId, String fileName) {
		String timestamp = DateUtil.getFormatDate(new Date(), "yyyyMMddHHmmss");
		return parentId + "/" + timestamp + "_" + fileName;
	}

	// 获取文件类型
	private String getFileType(String fileName) {
		String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
		// 根据扩展名判断文件类型
		if (Arrays.asList("jpg", "jpeg", "png", "gif").contains(extension)) {
			return "image";
		} else if (Arrays.asList("mp4", "avi", "mov", "webm").contains(extension)) {
			return "video";
		} else if (extension.equals("pdf")) {
			return "pdf";
		} else {
			return "other";
		}
	}

	@Operation(summary = "上传文件夹")
	@TokenRequired
	@PostMapping("/upload/folder")
	public R<String> uploadFolder(@RequestParam("files") MultipartFile[] files,
			@RequestParam("folderPath") String folderPath) {
		try {
			for (MultipartFile file : files) {
				String fileName = file.getOriginalFilename();
				String objectName = folderPath + "/" + fileName;

				minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName)
						.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build());
			}
			return R.ok("文件夹上传成功");
		} catch (Exception e) {
			log.error("文件夹上传失败", e);
			return R.failed("文件夹上传失败: " + e.getMessage());
		}
	}

	@Operation(summary = "上传文件分片")
	@TokenRequired
	@PostMapping("/upload/chunk")
	public R<Object> uploadChunk(
			HttpServletRequest request,
			@RequestParam("file") MultipartFile file,
			@RequestParam("chunkNumber") int chunkNumber,
			@RequestParam("totalChunks") int totalChunks,
			@RequestParam("identifier") String identifier,
			@RequestParam("parentId") String parentId
	) {
		try {
			// 创建临时目录用于存储分片
			String tempDir = System.getProperty("java.io.tmpdir") + "/disk_uploads/" + identifier;
			File dir = new File(tempDir);
			if (!dir.exists()) {
				dir.mkdirs();
			}

			// 保存分片文件
			String chunkPath = tempDir + "/chunk_" + chunkNumber;
			File chunk = new File(chunkPath);
			file.transferTo(chunk);

			// 检查是否所有分片都已上传
			if (isUploadComplete(tempDir, totalChunks)) {
				// 合并分片
				String fileName = file.getOriginalFilename();
				String objectName = generateObjectName(parentId, fileName);
				mergeChunks(tempDir, totalChunks, objectName);
				
				// 清理临时文件
				FileUtils.deleteDirectory(dir);
				
				// 保存文件记录
				Disk disk = new Disk();
				disk.setId(UuidUtil.getUUID());
				disk.setName(fileName);
				disk.setPath(objectName);
				disk.setSize(file.getSize());
				disk.setType(file.getContentType());
				disk.setParentId(parentId);
				disk.setCreateUserId(getUserId(request));
				disk.setCreateTime(new Date());
				
				if (disk.save()) {
					return R.ok(disk);
				}
			} else {
				// 返回当前上传进度
				return R.ok(Map.of(
					"chunkNumber", chunkNumber,
					"uploaded", getUploadedChunks(tempDir),
					"total", totalChunks
				));
			}

			return R.failed("上传失败");
		} catch (Exception e) {
			log.error("上传分片失败", e);
			return R.failed("上传失败: " + e.getMessage());
		}
	}

	@Operation(summary = "检查分片上传状态")
	@GetMapping("/upload/status")
	public R<Object> checkUploadStatus(
			@RequestParam("identifier") String identifier,
			@RequestParam("totalChunks") int totalChunks
	) {
		try {
			String tempDir = System.getProperty("java.io.tmpdir") + "/disk_uploads/" + identifier;
			File dir = new File(tempDir);
			if (!dir.exists()) {
				return R.ok(Map.of("uploaded", new ArrayList<>()));
			}

			List<Integer> uploadedChunks = getUploadedChunks(tempDir);
			return R.ok(Map.of("uploaded", uploadedChunks));
		} catch (Exception e) {
			log.error("检查上传状态失败", e);
			return R.failed(e.getMessage());
		}
	}

	// 检查是否所有分片都已上传
	private boolean isUploadComplete(String tempDir, int totalChunks) {
		return getUploadedChunks(tempDir).size() == totalChunks;
	}

	// 获取已上传的分片列表
	private List<Integer> getUploadedChunks(String tempDir) {
		File dir = new File(tempDir);
		File[] chunks = dir.listFiles((d, name) -> name.startsWith("chunk_"));
		List<Integer> uploadedChunks = new ArrayList<>();
		
		if (chunks != null) {
			for (File chunk : chunks) {
				String chunkName = chunk.getName();
				int chunkNumber = Integer.parseInt(chunkName.substring(6));
				uploadedChunks.add(chunkNumber);
			}
		}
		
		return uploadedChunks;
	}

	// 合并分片
	private void mergeChunks(String tempDir, int totalChunks, String objectName) throws Exception {
		File[] chunks = new File[totalChunks];
		for (int i = 0; i < totalChunks; i++) {
			chunks[i] = new File(tempDir + "/chunk_" + i);
		}

		// 创建临时合并文件
		File mergedFile = new File(tempDir + "/merged");
		try (OutputStream out = new FileOutputStream(mergedFile)) {
			byte[] buffer = new byte[2048];
			for (File chunk : chunks) {
				try (InputStream in = new FileInputStream(chunk)) {
					int len;
					while ((len = in.read(buffer)) != -1) {
						out.write(buffer, 0, len);
					}
				}
			}
		}

		// 上传到MinIO
		minioClient.putObject(
			PutObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.stream(new FileInputStream(mergedFile), mergedFile.length(), -1)
				.build()
		);

		// 删除临时合并文件
		mergedFile.delete();
	}

	@Operation(summary = "下载文件")
	@Parameters({ @Parameter(name = "objectName", description = "文件路径", required = true) })
	@TokenRequired
	@GetMapping("/download")
	public void download(HttpServletResponse response, 
			@RequestParam(value = "objectName", required = true, defaultValue = "")  String objectName) {
		try {
			// 获取文件流
			GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build());

			// 获取文件信息
			StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build());

			// 设置响应头
			response.setContentType(stat.contentType());
			response.setHeader("Content-Disposition", 
				"attachment;filename=" + objectName.substring(objectName.lastIndexOf("/") + 1));

			// 写入响应流
			try (InputStream in = object;
				 OutputStream out = response.getOutputStream()) {
				byte[] buffer = new byte[1024];
				int len;
				while ((len = in.read(buffer)) > 0) {
					out.write(buffer, 0, len);
				}
			}
		} catch (Exception e) {
			log.error("下载文件失败", e);
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	@Operation(summary = "获取文件列表")
	@Parameters({ @Parameter(name = "prefix", description = "路径前缀", required = false) })
	@TokenRequired
	@GetMapping("/list")
	public R<List<MinioFileInfo>> listFiles(
			@RequestParam(value = "prefix", required = false, defaultValue = "") String prefix) {
		try {
			List<MinioFileInfo> fileList = new ArrayList<>();
			Iterable<Result<Item>> results = minioClient
					.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(false).build());

			for (Result<Item> result : results) {
				Item item = result.get();
				MinioFileInfo fileInfo = new MinioFileInfo();
				fileInfo.setFileName(item.objectName());
				fileInfo.setSize(item.size());
				fileInfo.setDirectory(item.isDir());
				fileInfo.setLastModified(item.lastModified());
				fileList.add(fileInfo);
			}

			return R.ok(fileList);
		} catch (Exception e) {
			log.error("获取文件列表失败", e);
			return R.failed("获取文件列表失败: " + e.getMessage());
		}
	}

	@Operation(summary = "预览文件")
	@Parameters({ @Parameter(name = "objectName", description = "文件路径", required = true) })
	//@TokenRequired
	@GetMapping("/preview")
	public void preview(HttpServletRequest request,HttpServletResponse response, 
			@RequestParam(value = "objectName", required = true, defaultValue = "") String objectName) {
		try {
			// 获取文件信息
			StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build());

			// 获取文件流
			GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build());

			// 设置响应头
			response.setContentType(stat.contentType());
			response.setHeader("Cache-Control", "max-age=2592000");
			
			// 对于视频文件，添加范围请求支持
			String range = request.getHeader("Range");
			if (range != null && range.startsWith("bytes=")) {
				long fileSize = stat.size();
				String[] ranges = range.substring(6).split("-");
				long rangeStart = Long.parseLong(ranges[0]);
				long rangeEnd = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileSize - 1;
				
				response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
				response.setHeader("Accept-Ranges", "bytes");
				response.setHeader("Content-Range", String.format("bytes %d-%d/%d", rangeStart, rangeEnd, fileSize));
				response.setHeader("Content-Length", String.valueOf(rangeEnd - rangeStart + 1));
				
				// 跳过不需要的字节
				object.skip(rangeStart);
				
				// 写入指定范围的数据
				try (InputStream in = object;
					 OutputStream out = response.getOutputStream()) {
					byte[] buffer = new byte[1024];
					long bytesRemaining = rangeEnd - rangeStart + 1;
					int len;
					while (bytesRemaining > 0 && (len = in.read(buffer, 0, (int)Math.min(buffer.length, bytesRemaining))) > 0) {
						out.write(buffer, 0, len);
						bytesRemaining -= len;
					}
				}
			} else {
				// 普通文件直接写入响应流
				try (InputStream in = object;
					 OutputStream out = response.getOutputStream()) {
					byte[] buffer = new byte[1024];
					int len;
					while ((len = in.read(buffer)) > 0) {
						out.write(buffer, 0, len);
					}
				}
			}
		} catch (Exception e) {
			//log.error("预览文件失败", e);
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	// 修改获取缩略图的方法
	@Operation(summary = "获取缩略图")
	@Parameters({ 
		@Parameter(name = "objectName", description = "文件路径", required = true),
		@Parameter(name = "width", description = "缩略图宽度", required = false),
		@Parameter(name = "height", description = "缩略图高度", required = false)
	})
	//@TokenRequired
	@GetMapping("/thumbnail")
	public void getThumbnail(HttpServletResponse response,
			@RequestParam(value = "objectName", required = true) String objectName,
			@RequestParam(value = "width", required = false, defaultValue = "200") int width,
			@RequestParam(value = "height", required = false, defaultValue = "200") int height) {
		try {
			// 获取文件信息
			StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build());

			String contentType = stat.contentType();
			
			// 根据文件类型处理缩略图
			if (contentType.startsWith("image/")) {
				// 处理图片缩略图
				generateImageThumbnail(objectName, width, height, response);
			} else if (contentType.startsWith("video/")) {
				// 处理视频缩略图
				generateVideoThumbnail(objectName, width, height, response);
			} else {
				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				return;
			}
			
		} catch (Exception e) {
			log.error("生成缩略图失败: {}", e.getMessage(), e);
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	// 生成图片缩略图
	private void generateImageThumbnail(String objectName, int width, int height, HttpServletResponse response) throws Exception {
		try (GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
				.bucket(bucketName)
				.object(objectName)
				.build())) {
			
			response.setContentType("image/jpeg");
			response.setHeader("Cache-Control", "max-age=2592000");
			
			try (InputStream in = object;
				 OutputStream out = response.getOutputStream()) {
				Thumbnails.of(in)
					.size(width, height)
					.keepAspectRatio(true)
					.outputQuality(0.8)
					.outputFormat("jpeg")
					.toOutputStream(out);
			}
		}
	}

	// 生成视频缩略图
	private void generateVideoThumbnail(String objectName, int width, int height, HttpServletResponse response) throws Exception {
		// 创建临时文件
		File tempVideo = File.createTempFile("video_", ".tmp");
		File tempThumb = File.createTempFile("thumb_", ".jpg");
		
		try {
			// 下载视频文件到临时目录
			try (GetObjectResponse object = minioClient.getObject(GetObjectArgs.builder()
					.bucket(bucketName)
					.object(objectName)
					.build());
				 FileOutputStream fos = new FileOutputStream(tempVideo)) {
				
				byte[] buffer = new byte[1024];
				int len;
				while ((len = object.read(buffer)) > 0) {
					fos.write(buffer, 0, len);
				}
			}
			
			// 使用 FFmpeg 提取视频帧
			FFmpeg ffmpeg = new FFmpeg(ffmpegPath);
			FFprobe ffprobe = new FFprobe(ffprobePath);
			
			// 获取视频时长
			FFmpegProbeResult probeResult = ffprobe.probe(tempVideo.getAbsolutePath());
			double duration = probeResult.getFormat().duration;
			
			// 在视频 1/3 处截取一帧
			long timestamp = (long) (duration / 3);
			
			FFmpegBuilder builder = new FFmpegBuilder()
				.setInput(tempVideo.getAbsolutePath())
				.addOutput(tempThumb.getAbsolutePath())
				.setFrames(1)
				.setVideoFilter(String.format("scale=%d:%d", width, height))
				.setStartOffset(timestamp, TimeUnit.SECONDS)
				.done();
			
			FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
			executor.createJob(builder).run();
			
			// 返回生成的缩略图
			response.setContentType("image/jpeg");
			response.setHeader("Cache-Control", "max-age=2592000");
			
			try (FileInputStream fis = new FileInputStream(tempThumb);
				 OutputStream out = response.getOutputStream()) {
				byte[] buffer = new byte[1024];
				int len;
				while ((len = fis.read(buffer)) > 0) {
					out.write(buffer, 0, len);
				}
			}
			
		} finally {
			// 清理临时文件
			tempVideo.delete();
			tempThumb.delete();
		}
	}

}
